0x00 前情提要
上回书说到,我的笔记本在Linux系统下当亮度降低越过50%这个值时屏幕会黑掉。当时留下了一个疑问:为什么亮度在50%时BRIL寄存器的值恒为0?
最近得闲,继续追查这个问题。第一部分排查至此还剩下两个可能导致BRIL=0的因素:amdgpu驱动问题,或者其他软件组件/硬件本身问题。mmiotrace可以记录硬件驱动等对内存映射I/O(MMIO)的访问。我们可以用它来追踪软件层面对BRIL寄存器的修改。
0x01
首先检查一下Linux内核是否支持mmiotrace:
sudo zgrep '^CONFIG_MMIOTRACE=' /proc/config.gz
# CONFIG_MMIOTRACE=y支持的,可以准备追踪了。mmiotrace只能记录启用之后经ioremap()创建的映射,所以需要在amdgpu建立MMIO映射前开始追踪。重启进grub,先把amdgpu关掉。内核命令行加:
systemd.unit=multi-user.target modprobe.blacklist=amdgpuF10启动进入TTY,登录root账户。lsmod | grep amdgpu应当没有输出。
然后根据上一篇文章的伪代码:
GPUB = pci_config_read32(gpu, 0x24); // PCI BAR5
base = GPUB & 0xFFFFFF00;
addr = base + 0x0138;
BRIL = read8(addr + 0x01);
= (read32(base + 0x0138) >> 8) & 0xff;计算一下BRIL的物理地址:
BDF=0000:03:00.0
read BAR5_START BAR5_END BAR5_FLAGS < <(
sed -n '6p' /sys/bus/pci/devices/$BDF/resource
)
REG138=$((BAR5_START + 0x138))
BRIL_ADDR=$((BAR5_START + 0x139))
printf 'BAR5 = 0x%x\n' "$BAR5_START"
printf 'REG138 = 0x%x\n' "$REG138"
printf 'BRIL = 0x%x\n' "$BRIL_ADDR"
# BAR5 = 0xfd400000
# REG138 = 0xfd400138
# BRIL = 0xfd400139然后新建一个shell脚本。开启mmiotrace后它会把除了CPU0以外的所有核心全部下线,系统反应会比较迟钝,键盘上实时输入可能反应比较慢。
# stage1.sh
OUT=/var/tmp/mmiotrace.log
mount -t tracefs nodev /sys/kernel/tracing
# GPU MMIO 流量很大,给一个大的缓冲区
echo 65536 > /sys/kernel/tracing/buffer_size_kb
# 选择 mmiotrace
echo mmiotrace > /sys/kernel/tracing/current_tracer
# 持续消费日志
cat /sys/kernel/tracing/trace_pipe > $OUT &
CATPID=$!
echo 1 > /sys/kernel/tracing/tracing_on
echo "log: $OUT"
echo "reader PID: $CATPID"
# 然后加载amdgpu
modprobe amdgpu
# 驱动与背光设备出现
lsmod | grep amdgpu
ls /sys/class/backlight/# stage2.sh
MAX=$(cat /sys/class/backlight/amdgpu_bl1/max_brightness)
HALF=$(((MAX + 1) / 2))
echo $HALF > /sys/class/backlight/amdgpu_bl1/brightness
printf 'max=%s half=%s current=%s actual=%s\n' $MAX $HALF "$(cat /sys/class/backlight/amdgpu_bl1/brightness)" "$(cat /sys/class/backlight/amdgpu_bl1/actual_brightness)"
echo "brightness to half: $HALF" > /sys/kernel/tracing/trace_marker# stage3.sh
echo 0 > /sys/kernel/tracing/tracing_on
echo nop > /sys/kernel/tracing/current_tracer
sleep 1
kill $CATPID分别执行上述stage1.sh和stage2.sh,亮度会变为50%。然后等一两秒时间在终端内执行:
echo "Fn brightness down" > /sys/kernel/tracing/trace_marker执行后按下Fn亮度减,此时面板黑屏。
然后再按下Fn亮度加,并执行
echo "Fn brightness up" > /sys/kernel/tracing/trace_marker完成后执行stage3.sh停止捕获。检查一下有没有丢事件:
sudo grep -i lost $OUT
sudo dmesg | grep -i mmiotrace没有输出就是好事。如果丢了事件可以增加一下buffer大小,然后把开启捕获后的操作时间缩短一些。
然后把日志复制过来:
cp $OUT .一般是复制到/root。重启进正常系统,然后用root去筛选一下日志就好了,或者自己cd一下要复制到的目录。
0x02
重启进正常系统后,执行下述Python脚本筛日志:
import sys
log_path = '/path/to/mmiotrace.log'
target = int('0xfd400138', 0) # REG138
end = target + 4
with open(log_path, "r", errors="replace") as f:
for line in f:
fields = line.split()
if not fields:
continue
if fields[0] == "MARK": # 捕获时手动打的marker
print(line, end="")
continue
if fields[0] not in {"R", "W"} or len(fields) < 7: # 只需要读写操作
continue
try:
width = int(fields[1], 0)
address = int(fields[4], 0)
value = int(fields[5], 0)
except ValueError:
continue
if address < end and address + width > target: # 操作地址落在[target, target+4)
extra = ""
if width == 4 and address == target:
bril = (value >> 8) & 0xff
extra = f" decoded_BRIL=0x{bril:02x}"
'''
uint32_t r = readl(gpu_bar5 + 0x138);
uint8_t BRIL = (r >> 8) & 0xff;
'''
print(line.rstrip() + extra)根据Marker定位可以发现:
R 4 953.459481 32 0xfd400138 0x2000ff00 0x0 0 decoded_bril=0xff
W 4 953.459529 32 0xfd400138 0x20000000 0x0 0 decoded_bril=0x00在通过sys class将亮度手动设置为一半后,在Linux内发生了向BRIL寄存器写入0x00的操作。
倒回去计算一下。这台设备的亮度值为0-65535,而在50%(32768)时:
50% = 32768 = 0x8000
(0x8000 << 8) & 0x0000ff00
= 0x00800000 & 0x0000ff00
= 0换句话说,
BRIL = backlight_level & 0xff;背光等级是一个16位字段,与0xFF进行位运算等同于取低8位。在50%时,0xFF00就被截断成0x00。在这台设备的ACPI DSDT中,BRIL字段应当就是屏幕亮度的意思,在亮度为0后再次按下亮度减会关闭面板显示,逻辑合情合理。
按此思路去想,在Linux内将亮度降为0%时,再按下亮度减的预期表现应当是关闭显示,但实际上并没有。为什么呢?resource5+0x139 = 0x01 (1)
因为亮度为0%时实际亮度等级为1,哈哈。
目前的矛盾点已经明确:Linux(准确来说是amdgpu驱动)设置亮度时写入的是16位数据,而设备ACPI声明的数据长度是8位的。
OperationRegion (SCRA, SystemMemory, GBSA (), 0x04)
Field (SCRA, ByteAcc, NoLock, Preserve)
{
Offset (0x01),
BRIL, 8
}哪边是对的?
0x03
经过对amdgpu的一番大调查,发现其控制背光的代码如下:
// linux/drivers/gpu/drm/amd/amdgpu/amdgpu_atombios.c
void amdgpu_atombios_scratch_regs_set_backlight_level(
struct amdgpu_device *adev,
u32 backlight_level)
{
u32 tmp = RREG32(adev->bios_scratch_reg_offset + 2);
tmp &= ~ATOM_S2_CURRENT_BL_LEVEL_MASK;
tmp |= (backlight_level << ATOM_S2_CURRENT_BL_LEVEL_SHIFT) &
ATOM_S2_CURRENT_BL_LEVEL_MASK;
WREG32(adev->bios_scratch_reg_offset + 2, tmp);
}amdgpu使用寄存器号的形式访问寄存器,adev->bios_scratch_reg_offset + 2即表示amdgpu驱动中控制亮度的寄存器位于2号寄存器BIOS_SCRATCH_2。
而在AMD AtomBios里,对BIOS_SCRATCH_2的定义是:
// linux/drivers/gpu/drm/amd/include/atombios.h
#define ATOM_S2_CURRENT_BL_LEVEL_MASK 0x0000FF00L
#define ATOM_S2_CURRENT_BL_LEVEL_SHIFT 8
// 并另外给出面向BIOS的按字节定义
#define ATOM_S2_CURRENT_BL_LEVEL_MASKb1 0xFF0x0000FF00只覆盖bits 15:8,正好8 bit;右移8位,值域即为0x00-0xFF。
但是在2025年6月,AMD向amdgpu驱动加入了“向用户空间导出完整亮度范围“改动。此前,amdgpu向用户空间暴露0x00-0xFF,但在此patch之后用户空间可见范围扩大到了面板/PWM完整范围,通常为0x0000-0xFFFF,以提供更高的亮度调节精度。
[WHY] Userspace currently is offered a range from 0-0xFF but the PWM
is programmed from 0-0xFFFF. This can be limiting to some software
that wants to apply greater granularity.[HOW] Convert internally to firmware values only when mapping custom
brightness curves because these are in 0-0xFF range. Advertise full
PWM range to userspace.
但目前的amdgpu DM代码在这一更改后仍然执行
// linux/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
dm->brightness[bl_idx] = user_brightness;
amdgpu_atombios_scratch_regs_set_backlight_level(
dm->adev, dm->brightness[bl_idx]);它在把亮度转换成实际面板控制值之前,就把完整的用户亮度原样传给了只有8位的ATOM scratch字段,没有经过缩放。
于是,0xFF00被截断为0x00,导致在当前amdgpu驱动下50%亮度时固件认为亮度已经到达0%,再按一次亮度减关闭了面板。
要解决这个问题,缩放一下应该就可以了。
scratch_level = user_brightness * 255 / user_max_brightness比如,
static u8 amdgpu_dm_brightness_to_atombios_scratch(u32 brightness,
u32 max_brightness)
{
const u32 scratch_max =
ATOM_S2_CURRENT_BL_LEVEL_MASK >>
ATOM_S2_CURRENT_BL_LEVEL_SHIFT;
u32 level;
if (!brightness || !max_brightness)
return 0;
brightness = min(brightness, max_brightness);
level = DIV_ROUND_CLOSEST_ULL((u64)brightness * scratch_max,
max_brightness);
return level;
}然后
static void amdgpu_dm_backlight_set_level(
struct amdgpu_display_manager *dm,
int bl_idx,
u32 user_brightness)
{
struct amdgpu_dm_backlight_caps *caps;
struct dc_link *link;
u32 brightness;
bool rc, reallow_idle = false;
amdgpu_dm_update_backlight_caps(dm, bl_idx);
caps = &dm->backlight_caps[bl_idx];
dm->brightness[bl_idx] = user_brightness;
/* update scratch register */
- if (bl_idx == 0)
+ if (bl_idx == 0) {
+ struct backlight_device *bd = dm->backlight_dev[bl_idx];
+ u32 max_brightness = bd ? bd->props.max_brightness : AMDGPU_MAX_BL_LEVEL;
+ u8 scratch_level;
+
+ scratch_level =
+ amdgpu_dm_brightness_to_atombios_scratch(
+ user_brightness, max_brightness);
+
amdgpu_atombios_scratch_regs_set_backlight_level(
- dm->adev, dm->brightness[bl_idx]);
+ dm->adev, scratch_level);
+ }
brightness = convert_brightness_from_user(
caps, dm->brightness[bl_idx]);上述补丁应该是有效的。但是编译Linux内核有点消耗资源,过几天再验证。